#include "ShaderProgram.h"
#include "OpenGLException.h"

#include <fstream>
#include <sstream>

#include <glad/glad.h>
#include <glm/gtc/type_ptr.hpp>

ShaderProgram::ShaderProgram(const std::string &vsPath, const std::string &fsPath)
{
    std::string vs = loadShaderSource(vsPath);
    std::string fs = loadShaderSource(fsPath);
    m_rendererId   = createShaderProgram(vs, fs);
}

ShaderProgram::~ShaderProgram()
{
    glDeleteProgram(m_rendererId);
}

std::string ShaderProgram::loadShaderSource(std::string filepath)
{
    std::ifstream stream(filepath);
    if (!stream.is_open())
        throw OpenGLException("shader file not exists!");

    std::ostringstream content;
    content << stream.rdbuf();
    return content.str();
}

unsigned int ShaderProgram::compileShader(unsigned int type, const std::string &source)
{
    unsigned int id  = glCreateShader(type);
    const char  *src = source.c_str();
    glShaderSource(id, 1, &src, NULL);
    glCompileShader(id);

    int success;
    glGetShaderiv(id, GL_COMPILE_STATUS, &success);
    if (!success) {
        char errmsg[1024] = {0};
        glGetShaderInfoLog(id, 1024, NULL, errmsg);
        glDeleteShader(id);
        std::string message =
            "Failed to compile " + std::string(type == GL_VERTEX_SHADER ? "vertex" : "fragment") + " shader: \n" + std::string(errmsg);
        throw OpenGLException(message);
    }
    return id;
}

unsigned int ShaderProgram::createShaderProgram(const std::string &vertexShader, const std::string &fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs      = compileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs      = compileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    int success;
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    if (!success) {
        char errmsg[1024] = {0};
        glGetProgramInfoLog(program, 1024, NULL, errmsg);
        glDeleteProgram(program);
        std::string message = "Failed to link shader program: " + std::string(errmsg);
        throw OpenGLException(message);
    }

    glDeleteShader(vs);
    glDeleteShader(fs);
    return program;
}

int ShaderProgram::getUniformLocation(const std::string &name)
{
    if (m_uniformLocationCache.find(name) != m_uniformLocationCache.end())
        return m_uniformLocationCache.at(name);

    int location = glGetUniformLocation(m_rendererId, name.c_str());
    if (location == -1) {
        std::string message = "Uniform " + name + " doesn't exists!";
        throw OpenGLException(message);
    }
    m_uniformLocationCache[name] = location;
    return location;
}

void ShaderProgram::setUniform1i(const std::string &name, int value)
{
    int location = getUniformLocation(name);
    glUniform1i(location, value);
}

void ShaderProgram::setUniform1f(const std::string &name, float value)
{
    int location = getUniformLocation(name);
    glUniform1f(location, value);
}

void ShaderProgram::setUniformVec3f(const std::string &name, const glm::vec3 &vec)
{
    int location = getUniformLocation(name);
    glUniform3fv(location, 1, glm::value_ptr(vec));
}

void ShaderProgram::setUniform3f(const std::string &name, float x, float y, float z)
{
    int location = getUniformLocation(name);
    glUniform3f(location, x, y, z);
}

void ShaderProgram::setUniformMat4fv(const std::string &name, const glm::mat4 &matrix)
{
    int location = getUniformLocation(name);
    glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
}

void ShaderProgram::bind()
{
    glUseProgram(m_rendererId);
}

void ShaderProgram::unbind()
{
    glUseProgram(0);
}
